package org.iabako.server.serviceimpl.business;

import org.apache.log4j.Logger;
import org.iabako.server.dao.JpaDAO;
import org.iabako.server.dao.business.ProductDAO;
import org.iabako.server.dao.business.QueryDefinitionDAO;
import org.iabako.server.dao.business.SaleDAO;
import org.iabako.server.dao.business.ServiceDAO;
import org.iabako.server.tools.ServerTools;
import org.iabako.shared.entity.business.*;
import org.iabako.shared.entity.enumeration.*;
import org.iabako.shared.entity.user.Enterprise;
import org.iabako.shared.exception.IabakoActionForbiddenException;
import org.iabako.shared.exception.IabakoPackageForbiddenException;
import org.iabako.shared.exception.IabakoStockException;
import org.iabako.shared.exception.IabakoUniqueConstraintException;
import org.iabako.shared.iservice.business.*;
import org.iabako.shared.iservice.user.UserService;
import org.iabako.shared.querydefinition.SearchMotorQueryEnum;
import org.iabako.shared.querydefinition.columndefinition.ClientColumnEnum;
import org.iabako.shared.querydefinition.columndefinition.ColumnDefinition;
import org.iabako.shared.querydefinition.columndefinition.SaleColumnEnum;
import org.iabako.shared.querydefinition.dto.QueryDefinitionDTO;
import org.iabako.shared.querydefinition.dto.QueryOperatorEnum;
import org.iabako.shared.querydefinition.dto.QueryResultDTO;
import org.iabako.shared.tools.GenericTools;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
 * Created by jose on 26/08/14.
 */

@org.springframework.stereotype.Service("saleService")
public class SaleServiceImpl extends AbstractServiceImpl implements SaleService {

    static Logger log = Logger.getLogger(SaleServiceImpl.class.getName());

    @Autowired
    private SaleDAO saleDAO;
    @Autowired
    private ProductDAO productDAO;
    @Autowired
    private ServiceDAO serviceDAO;
    @Autowired
    private InstallmentService installmentService;
    @Autowired
    private UserService userService;
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private ClientService clientService;
    @Autowired
    private QueryDefinitionDAO queryDefinitionDAO;
    @Autowired
    private TrackingService trackingService;

    @Override
    protected JpaDAO getDAO() {
        return saleDAO;
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public List<Payment> getPayments(Sale sale) {
        if (sale == null || sale.getId() == null) {
            return null;
        }
        return saleDAO.getPayments(sale);
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public List<Installment> getInstallments(Sale sale) {
        if (sale == null || sale.getId() == null) {
            return null;
        }
        return saleDAO.getInstallments(sale);
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public Sale calculateStatusAndSave(Sale sale, boolean isDraft) {
        sale.calculateStatus(isDraft);
        sale = (Sale) super.save(sale);
        if (!isDraft) {
            clientService.calculateStatus(sale.getClient());
        }
        return sale;
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void paymentToInstallment(Long idPayment) {

        Payment payment = (Payment) paymentService.getById(idPayment);
        if (payment == null) {
            log.warn("No payment found with id :" + idPayment);
            return;
        }
        Installment installment = new Installment();
        installment.setDate(payment.getScheduleDate() != null ? payment.getScheduleDate() : payment.getDate());
        installment.setAmount(payment.getAmount());
        installment.setSale(payment.getSale());
        installment.setComment(payment.getComment());

        paymentService.remove(payment, true);
        installmentService.save(installment, true);

        trackingService.addTrackingToUserSession(TrackingType.paymentReceivedToInstallment, payment.getSale());
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void installmentToPayment(Installment installment) throws IabakoPackageForbiddenException {

        Installment installmentFromDB = (Installment) installmentService.getById(installment.getId());
        if (installmentFromDB == null) {
            log.warn("No installment found with id :" + installmentFromDB.getId());
            return;
        }

        Payment payment = new Payment();
        payment.setDate(GenericTools.getDateFromString(installment.getDateString()));
        payment.setScheduleDate(GenericTools.getDateFromString(installment.getScheduleDateString()));
        payment.setAmount(installment.getAmount());
        payment.setSale(installment.getSale());
        payment.setComment(installment.getComment());
        payment.setPaymentMethod(installment.getPaymentMethod());

        installmentService.remove(installmentFromDB, true);
        paymentService.save(payment, true);

        trackingService.addTrackingToUserSession(TrackingType.installmentToPaymentReceived, installment.getSale());
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public Boolean remove(Long id) {

        saleDAO.deleteAttachedObjects(id);

        Sale sale = saleDAO.findById(id);
        if (sale != null) {
            log.warn("No sale found with id :" + id);
            return false;
        }

        Client client = sale.getClient();

        boolean result = super.remove(id);

        if (client != null) {
            clientService.calculateStatus(client);
        }

        trackingService.addTrackingToUserSession(TrackingType.saleDelete, sale);

        return result;
    }

    /**
     * This method is used by SaleModificationForm
     *
     * @param sale
     * @return
     * @throws IabakoActionForbiddenException
     */
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public Sale save(Sale sale) throws IabakoActionForbiddenException, IabakoUniqueConstraintException {

        Enterprise enterprise = getEnterpriseFromSessionUser();
        if (sale.getId() != null && enterprise.getAllRelatedEnterprises().size() > 1) {
            //Do not change Enterprise!
            sale.setEnterprise(saleDAO.findById(sale.getId()).getEnterprise());
        } else {
            sale.setEnterprise(enterprise);
        }

        if (enterprise != null && enterprise.isDemo()) {
            throw new IabakoActionForbiddenException("action_forbidden_for_demo_title", "action_forbidden_for_demo_text", true);
        }

        if (!getDAO().isUniqueNumber(sale)) {
            throw new IabakoUniqueConstraintException(
                    messages.getLabel("validation_sale_unique_constraint_error_title"),
                    messages.getLabel("validation_sale_unique_constraint_error_text", sale.getNumber()), true, true);
        }

        Sale saleFromDB = saleDAO.findById(sale.getId());
        setAttributesFromDB(sale, saleFromDB);

        sale.setDate(GenericTools.getDateFromString(sale.getDateString()));

        trackingService.addTrackingToUserSession(TrackingType.saleModify, sale);

        return rawSave(sale);
    }

    private void setAttributesFromDB(Sale sale, Sale saleFromDB) {
        sale.setStatus(saleFromDB.getStatus());
        sale.setRecalculatedTotal(saleFromDB.getRecalculatedTotal());
    }

    public Sale rawSave(Sale sale) {
        setBusinessTrackInfo(sale);
        return (Sale) super.save(sale);
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public List<Installment> getCustomInstallmentsDefinition(Sale sale) {
        if (sale == null || sale.getId() == null) {
            return null;
        }
        return saleDAO.getCustomInstallmentsDefinition(sale);
    }

    public List<SaleProductService> getProductServiceList(Sale sale) {
        return saleDAO.getProductServiceList(sale);
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public Sale stopUndefinedInstallmentsCreation(Sale sale) throws IabakoActionForbiddenException {
        InstallmentsDefinition iDef = sale.getInstallmentsDefinition();
        Date lastUndefinedInstallment = iDef.addNToDate(new ServerTools(), iDef.getNextUndefinedInstallment(), iDef.getInstallmentsPeriodicalFrequency(), iDef.getInstallmentsPeriodicalFrequencyN() * -1);

        sale.getInstallmentsDefinition().setNextUndefinedInstallment(null);
        sale.getInstallmentsDefinition().setStopUndefinedInstallments(new Date());
        if (lastUndefinedInstallment.compareTo(iDef.getInstallmentsPeriodicalStartDate()) >= 0) {
            sale.getInstallmentsDefinition().setLastUndefinedInstallment(lastUndefinedInstallment);
        }

        try {
            return save(sale);
        } catch (IabakoUniqueConstraintException e) {
            //Should never happen
            log.error(e.getMessage(), e);
        }
        return sale;
    }

    /**
     * Method to ALWAYS USED IN SALE MAKE (PERSIST AND DRAFT MODE)
     * We can't pass by cascading persist of SaleProductServices, Payments and Installments because GWT does not support (serialize)
     * PersistentBag and Proxy/Lazy hibernate objects
     *
     * @param sale         Sale object from GWT client (navigator/JS world)
     * @param payments     List of Payment from GWT client (navigator/JS world)
     * @param installments List of Installments from GWT client (navigator/JS world)
     */
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public synchronized Sale save(boolean isDraft, Sale sale, List<SaleProductService> productsServices, List<Payment> payments, List<Installment> installments)
            throws IabakoActionForbiddenException,
            IabakoPackageForbiddenException,
            IabakoUniqueConstraintException,
            IabakoStockException {

        Enterprise enterprise = getEnterpriseFromSessionUser();

        if (enterprise != null && enterprise.isDemo()) {
            throw new IabakoActionForbiddenException("action_forbidden_for_demo_title", "action_forbidden_for_demo_text", true);
        }

        if (!getDAO().isUniqueNumber(sale)) {
            throw new IabakoUniqueConstraintException(
                    messages.getLabel("validation_sale_unique_constraint_error_title"),
                    messages.getLabel("validation_sale_unique_constraint_error_text", sale.getNumber()), true, true);
        }

        if (sale.getEnterprise() == null) {
            sale.setEnterprise(enterprise);
        }

        if (sale.getClient() != null && sale.getClient().getEnterprise() == null) {
            sale.getClient().setEnterprise(enterprise);
        }

        sale.setDate(GenericTools.getDateFromString(sale.getDateString()));

        addProductsServicesToSale(isDraft, sale, productsServices);
        addCustomInstallmentsDefinition(sale, installments);

        if (!isDraft) {
            addPaymentsInstallments(sale, payments, installments);
            sale.setRecalculatedTotal(sale.calculateAmountFromPaymentsAndInstalments());
            sale.setTaxPercent(sale.getPonderedTaxPercent());
        } else {
            sale.setRecalculatedTotal(sale.calculateAmountFromProductsServices());
        }

        boolean isCreation = sale.getId() == null;
        sale = calculateStatusAndSave(sale, isDraft);
        setBusinessTrackInfo(sale);

        Sale quote = checkIfQuote(sale);

        if (isCreation && quote != null) {
            trackingService.addTrackingToUserSession(TrackingType.quoteTransformed, quote);

        } else if (isDraft) {
            trackingService.addTrackingToUserSession(TrackingType.saleDraft, sale);

        } else {
            trackingService.addTrackingToUserSession(TrackingType.saleNew, sale);
            userService.addInvoiceReceiptToEnterpriseBalance(sale, null);
        }

        return sale;
    }

    private Sale checkIfQuote(Sale sale) {
        if (sale.getSaleQuoteId() != null) {
            Sale saleQuote = saleDAO.findById(Long.parseLong(sale.getSaleQuoteId()));
            if (saleQuote != null && saleQuote.isQuote()) {
                saleQuote.setSaleQuoteId(sale.getStatus() == FinancialStatusSale._0_draft ? "_draft_" + sale.getId() : sale.getId() + "");
                saleQuote.setStatus(FinancialStatusSale._2_quote_transformed);
            } else {
                sale.setSaleQuoteId(null);
            }
            saleDAO.merge(saleQuote);
            return saleQuote;
        }
        return null;
    }

    private void addCustomInstallmentsDefinition(Sale sale, List<Installment> installments) {
        if (sale.getInstallmentsDefinition() == null || sale.getInstallmentsDefinition().getInstallmentsType() == null
                || sale.getInstallmentsDefinition().getInstallmentsFrequency() == null) {
            sale.setPaymentType(PaymentType.onePayment);
            sale.setInstallmentsDefinition(null);

        } else {
            InstallmentsDefinition installDef = sale.getInstallmentsDefinition();

            sale.setPaymentMethod(null);

            if (installDef.getInstallmentsFrequency() == InstallmentsFrequency.periodical) {
                installDef.setInstallmentsPeriodicalStartDate(GenericTools.getDateFromString
                        (installDef.getInstallmentsPeriodicalStartDateString()));

            } else {
                //Clear periodical values
                installDef.setInstallmentsPeriodicalStartDate(null);
                installDef.setInstallmentsPeriodicalPrice(null);
            }

            if (sale.getInstallmentsDefinition().getInstallmentsFrequency() == InstallmentsFrequency.custom) {

                List<Installment> customInstallmentDefList = new ArrayList<Installment>();

                for (Installment installment : installments) {
                    Installment customInstallmentDef = new Installment();
                    customInstallmentDef.setDate(GenericTools.getDateFromString(installment.getDateString()));
                    //In drafts, amount can be null (but in database)
                    customInstallmentDef.setAmount(installment.getAmount() == null ? 0 : installment.getAmount());
                    customInstallmentDef.setInstallmentsDefinition(sale.getInstallmentsDefinition());
                    customInstallmentDefList.add(customInstallmentDef);
                }

                sale.getInstallmentsDefinition().setInstallments(customInstallmentDefList);

            } else if (sale.getInstallmentsDefinition().getInstallmentsType() == InstallmentsType.undefined) {

                //Clear defined value
                installDef.setInstallmentsNumber(0);

                Date lastInstallmentDate = installments.isEmpty() ?
                        sale.getInstallmentsDefinition().getInstallmentsPeriodicalStartDate()
                        : installments.get(installments.size() - 1).getDate();

                Date nextUndefinedInstallment;
                if (installments.isEmpty()) {
                    nextUndefinedInstallment = lastInstallmentDate;

                } else {
                    nextUndefinedInstallment = sale.getInstallmentsDefinition().addNToDate(
                            new ServerTools(),
                            lastInstallmentDate,
                            sale.getInstallmentsDefinition().getInstallmentsPeriodicalFrequency(),
                            sale.getInstallmentsDefinition().getInstallmentsPeriodicalFrequencyN());
                }
                sale.getInstallmentsDefinition().setNextUndefinedInstallment(nextUndefinedInstallment);
            }
        }
    }

    public void addProductsServicesToSale(boolean isDraft, Sale saleFromDB, List<SaleProductService> productsServices) throws IabakoStockException {

        Enterprise enterprise = getEnterpriseFromSessionUser();
        String productsServicesAsString = "";

        Map<Product, Double> stockReductionMap = new HashMap<Product, Double>();

        //1. Create productService as String (for table results)
        saleFromDB.setProductsServicesAsString(null);

        saleDAO.cleanProductService(saleFromDB);

        for (SaleProductService productService : productsServices) {

            String quantityString = productService.getQuantity() == 0d
                    || productService.getQuantity() / new Double(productService.getQuantity()).intValue() == 1
                    ? new Double(productService.getQuantity()).intValue() + ""
                    : ServerTools.formatAmount(saleFromDB.getEnterprise().getLanguage(), productService.getQuantity());

            if (productService.getProduct() != null) {

                Double quantity = stockReductionMap.get(productService.getProduct());
                stockReductionMap.put(productService.getProduct(),
                        quantity == null ? productService.getQuantity() : quantity + productService.getQuantity());

                quantityString += productService.getProduct().getPriceUnit() == PriceUnit.unit ? "" : messages.getLabel(productService.getProduct().getPriceUnit().getLabelKey());

                productsServicesAsString += productService.getProduct().getName()
                        + " (" + quantityString + ")\n";
            } else {
                productsServicesAsString += productService.getService().getName()
                        + " (" + quantityString + ")\n";
            }
        }
        if (!GenericTools.isEmpty(productsServicesAsString)) {
            productsServicesAsString = productsServicesAsString.substring(0, productsServicesAsString.lastIndexOf("\n"));
            productsServicesAsString = productsServicesAsString.length() > 1000 ?
                    productsServicesAsString.substring(0, 1000) : productsServicesAsString;
            saleFromDB.setProductsServicesAsString(productsServicesAsString);
            saleFromDB.setProductsServices(productsServices);
        }

        //2. Reset Product/Service Enterprise (and do not change Enterprise!)
        boolean enterpriseHasChildren = enterprise.getAllRelatedEnterprises().size() > 1;
        Product prod;
        Service service;
        for (SaleProductService saleProductService : saleFromDB.getProductsServices()) {
            if ((prod = saleProductService.getProduct()) != null && saleProductService.getProduct().getEnterprise() == null) {
                saleProductService.getProduct().setEnterprise(enterpriseHasChildren ?
                        productDAO.findById(prod.getId()).getEnterprise() : enterprise);

            } else if ((service = saleProductService.getService()) != null && saleProductService.getService().getEnterprise() == null) {
                saleProductService.getService().setEnterprise(enterpriseHasChildren ?
                        serviceDAO.findById(service.getId()).getEnterprise() : enterprise);
            }
        }

        if (isDraft) {
            return;
        }

        //3. Stock reduction
        for (Product product : stockReductionMap.keySet()) {
            if (product.getQuantity() < stockReductionMap.get(product)) {
                String unit = messages.getLabel(product.getPriceUnit().getLabelKey());
                unit = GenericTools.isEmpty(unit) ? " " + messages.getLabel("stock_unit") : unit;

                String stringQuantity = product.getQuantity() == 0d
                        || product.getQuantity() / new Double(product.getQuantity()).intValue() == 1 ?
                        new Double(product.getQuantity()).intValue() + ""
                        : ServerTools.formatAmount(saleFromDB.getEnterprise().getLanguage(), product.getQuantity());

                String message = messages.getLabel("stock_sale_not_enough_text", product.getName(), stringQuantity, unit);
                throw new IabakoStockException("stock_sale_not_enough_title", message, true);
            }

            product.setQuantity(ServerTools.round(product.getQuantity() - stockReductionMap.get(product)));
            productDAO.merge(product);
        }
    }

    private void addPaymentsInstallments(Sale sale, List<Payment> payments, List<Installment> installments) {
        if (installments != null) {
            for (Installment i : installments) {
                i.setDate(GenericTools.getDateFromString(i.getDateString()));
                i.setAmount(ServerTools.round(i.getAmount()));
            }
            if (sale.getClient() != null) {
                //Client must be enabled if he has future or pending installments
                sale.getClient().setDisabled(false);
            }
        }

        if (payments != null) {
            for (Payment p : payments) {
                p.setDate(GenericTools.getDateFromString(p.getDateString()));
                p.setScheduleDate(GenericTools.getDateFromString(p.getScheduleDateString()));
                p.setAmount(ServerTools.round(p.getAmount()));
            }
        }

        sale.setInstallments(installments);
        sale.setPayments(payments);
    }

    public QueryDefinitionDTO getSearchQueryDefinitionDTO(QueryDefinitionDTO qd) {
        qd.setSearchMotorQuery(SearchMotorQueryEnum.SaleSearch);
        qd.addCriteria(SaleColumnEnum.saleQuote, QueryOperatorEnum.EQUALS, false);
        qd.addCriteria(SaleColumnEnum.saleEnterprise, QueryOperatorEnum.IN, (ArrayList) getSessionUser().getEnterprise().getVisibleEnterprisesFinancialData());
        queryDefinitionDAO.executeQueryDefinition(qd);

        for (QueryResultDTO result : qd.getResults().values()) {
            if (result.getValues().get(SaleColumnEnum.status).getValue() == FinancialStatusSale._0_draft) {
                result.setIdPrefix("_draft_");
            }
        }

        qd.setCustomModifications(true);

        return qd;
    }

    public QueryDefinitionDTO setSearchCriteriaFromSuggest(QueryDefinitionDTO qd, Long hiddenSelectionCode, String suggestionValue) {
        if (!GenericTools.isEmpty(hiddenSelectionCode)) {
            qd.addCriteria(SaleColumnEnum.clientId, QueryOperatorEnum.EQUALS, hiddenSelectionCode);
            return qd;
        }
        //email, address1 can be got from ClientColumnEnum because left outer join is already put in request by clientNumber
        return setSearchCriteriaFromSuggest(qd,
                suggestionValue,
                new ColumnDefinition[]{SaleColumnEnum.saleNumber, SaleColumnEnum.clientNumber, SaleColumnEnum.client, ClientColumnEnum.email, ClientColumnEnum.address1});
    }
}
